在上一篇文章,使用一個入門範例示範如何建立 Kernel 以及與 LLMs 進行對話,在那個範例裡並沒有涉及到使用 OpenAI GPT 模型常用的 System Prompt、User Prompt,今天的內容就來看看如何建立一個具有 System Prompt、User Prompt 的對話,以及探討建立 Kernel的不同寫法。
在開始之前,先談一下建立 Kernel 的變化寫法,回顧一下前一篇文章的範例是這樣建立的,在建立 Kernel 物件後,調用 AddOpenAIChatCompletion 方法。
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: Config.openai_modelId,
apiKey: Config.openai_apiKey)
.Build();
此外,另一種可行的方式是先建立 builder,再透過公開的 Services 屬性來新增服務。這種方法的彈性在於,當應用程式需要根據不同的功能情境來配置不同服務時,能夠提供更好的靈活性,產生更符合情境所需要的 Kernel 物件 。
var builder = Kernel.CreateBuilder();
builder.Services.AddAzureOpenAIChatCompletion(
endpoint: Config.aoai_endpoint,
deploymentName: Config.aoai_deployment,
apiKey: Config.aoai_apiKey);
var kernel = builder.Build();
如果仔細研究 OpenAI API,你會發現,在 API 的請求(Request)和回應(Response)中,Message 物件會包含一個 role 屬性,而它的值可以是 system、user、assistant(在新版本中還新增了 tool,但這裡先不討論)。
以下是各個 role 的說明:
那麼當我們想依循具有 role 機制的方式建立對話內容時,應該怎麼寫?在 kernel.InvokePromptAsync 置入特定格式即可。
Kernel kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
endpoint: Config.aoai_endpoint,
deploymentName: Config.aoai_deployment,
apiKey: Config.aoai_apiKey)
.Build();
string chatPrompt = """
<message role="system">你是一位專業的C#程式專家,協助我進行C#程式的開發工作</message>
<message role="user">你能說說什麼是非同步方法嗎</message>
""";
Console.WriteLine(await kernel.InvokePromptAsync(chatPrompt));
但這樣的寫法讓 Prompt 變的有些凌亂,我們進一步改用以下的寫法
Kernel kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
endpoint: Config.aoai_endpoint,
deploymentName: Config.aoai_deployment,
apiKey: Config.aoai_apiKey)
.Build();
ChatHistory chatHistory = [];
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
chatHistory.AddSystemMessage("你是一位專業的C#程式專家,協助我進行C#程式的開發工作");
chatHistory.AddUserMessage("你能說說什麼是非同步方法嗎");
Console.WriteLine(await chatCompletionService.GetChatMessageContentAsync(chatHistory: chatHistory, kernel: kernel));
這種寫法先建立 ChatHistory 物件,用於收集連續對話的記錄,作用於實現短期對話記錄的記憶機制,接著透過 GetRequiredService 方法取得 ChatCompletionService 物件。根據 Prompt 的內容,使用 AddSystemMessage 和 AddUserMessage 方法將訊息加入對話記錄(ChatHistory)。最後,再透過 ChatCompletionService 物件的 GetChatMessageContentAsync 方法與 OpenAI 模型進行互動。相比之下,這種寫法比使用 kernel.InvokePromptAsync 更加清晰。
此外這個寫法,也許可能會有個疑惑是 GetRequiredService,在上述程式中 kernel 物件的建立並沒有加入 Service 啊,那為什麼 GetRequiredService 卻可以取得 IChatCompletionService 的實作呢?其實在建立 Kernel 物件時 AddAzureOpenAIChatCompletion 這個方法的內部,做了這件事
Func<IServiceProvider, object?, AzureOpenAIChatCompletionService> factory = (serviceProvider, _) =>
{
AzureOpenAIClient client = CreateAzureOpenAIClient(
endpoint,
new AzureKeyCredential(apiKey),
HttpClientProvider.GetHttpClient(httpClient, serviceProvider));
return new(deploymentName, client, modelId, serviceProvider.GetService<ILoggerFactory>());
};
builder.Services.AddKeyedSingleton<IChatCompletionService>(serviceId, factory);
也就是會根據 AddAzureOpenAIChatCompletion 的情況,在內部自動加入了 AzureOpenAIChatCompletionService 服務, 而 AzureOpenAIChatCompletionService 就是 IChatCompletionService 的實作類別。
建立 Kernel 大致可以分成 2 種方式,方式1 是一次就配置好,適合不需要依情境條件而產生 kernel 物件的場景,相對的方式2 提供了可以依情境條件而產生 kernel 物件的場景,開發者可以依情況做不同的選擇。此外 ChatCompletionService 搭配 ChatHistory 可以具體實現 OpenAI API中關於 Role Prompt 的機制以及短期對話記錄的記憶。
Kernel kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
endpoint: Config.aoai_endpoint,
deploymentName: Config.aoai_deployment,
apiKey: Config.aoai_apiKey)
.Build();
var builder = Kernel.CreateBuilder();
if (config.IsAzureOpenAIConfigured)
{
// Use Azure OpenAI Deployments
builder.Services.AddAzureOpenAIChatCompletion(
config.AzureOpenAI!.DeploymentName!,
config.AzureOpenAI.Endpoint!,
config.AzureOpenAI.ApiKey!);
}
else
{
// Use OpenAI
builder.Services.AddOpenAIChatCompletion(
config.OpenAI!.ModelId!,
config.OpenAI.ApiKey!,
config.OpenAI.OrgId);
}
var kernel = builder.Build();